﻿using Bot.Dynamics365.Model;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Bot.Dynamics365
{
    public class Dynamics365Service : IDynamics365Service
    {
        private readonly IWebRequestService _webRequestService;
        private readonly ILogger<Dynamics365Service> _logger;

        public Dynamics365Service(IWebRequestService webRequestService,
            ILogger<Dynamics365Service> logger)
        {
            _webRequestService = webRequestService;
            _logger = logger;
        }

        
        public async Task<Dynamics365Response> GetDynamics365EntitysAsync(AzureAD azure)
        {
            var httpResponse = await _webRequestService.SendAsync(azure, "/api/data/v9.0/EntityDefinitions?$select=DisplayName,LogicalName,EntitySetName", Enum.EnumHttpMethod.Get);
            var dynamics365Response = ConvertToDynamics365Response(httpResponse);
            if (dynamics365Response.IsSuccess)
            {
                var dynamics365EntityResponse = JsonConvert.DeserializeObject<Dynamics365EntityResponse>(httpResponse.Response);
                dynamics365Response.Payload = ConvertToDynamics365Entitys(dynamics365EntityResponse);
            }
            return dynamics365Response;
        }

        public async Task<Dynamics365Response> GetDynamics365FieldsAsync(AzureAD azure, string entity)
        {
            var httpResponse = await _webRequestService.SendAsync(azure, $"/api/data/v9.0/EntityDefinitions(LogicalName='{entity}')?$select=LogicalName&$expand=Attributes", Enum.EnumHttpMethod.Get);
            var dynamics365Response = ConvertToDynamics365Response(httpResponse);
            if (dynamics365Response.IsSuccess)
            {
                var dynamics365EntityResponse = JsonConvert.DeserializeObject<Dynamics365EntityDescribeResponse>(httpResponse.Response);
                dynamics365Response.Payload = await ConvertToDynamics365FieldsAsync(azure, dynamics365EntityResponse);
            }
            return dynamics365Response;
        }

        public async Task<Dynamics365Response> QueryAsync(AzureAD azure, string entitySetName, string xml)
        {
            var httpResponse = await ExecFetchXmlAsync(azure, entitySetName, xml);
            var dynamics365Response = ConvertToDynamics365Response(httpResponse);
            if (dynamics365Response.IsSuccess)
            {
                var queryResponse = JsonConvert.DeserializeObject<QueryResponse>(httpResponse.Response);
                dynamics365Response.Payload = queryResponse.value;
            }
            return dynamics365Response;
        }

        public async Task<Dynamics365Response> QueryFirstAsync(AzureAD azure, string entitySetName, string xml)
        {
            var httpResponse = await ExecFetchXmlAsync(azure, entitySetName, xml);
            var dynamics365Response = ConvertToDynamics365Response(httpResponse);
            if (dynamics365Response.IsSuccess)
            {
                var queryResponse = JsonConvert.DeserializeObject<QueryResponse>(httpResponse.Response);
                dynamics365Response.Payload = ConvertToDynamics365FieldValues(queryResponse?.value.FirstOrDefault());
            }
            return dynamics365Response;
        }

        public async Task<Dynamics365Response> SearchFirstAsync(AzureAD azure, SearchInput input)
        {
            var xml = BuildFetchXml(input, 1);
            return await QueryFirstAsync(azure, input.EntitySetName, xml);
        }

        public async Task<Dynamics365Response> CreateEntityAsync(AzureAD azure, CreateEntityInput input)
        {
            var httpResponse = await CreateAsync(azure, input.EntitySetName, input.Content);
            var salesforceResponse = ConvertToDynamics365Response(httpResponse);

            if (!salesforceResponse.IsSuccess
                 && input.IfUpdateDup
                 && salesforceResponse.ErrorCode == "0x80040333")
            {
                httpResponse = await CreateAsync(azure, input.EntitySetName, input.Content, true);
                salesforceResponse = ConvertToDynamics365Response(httpResponse);
            }

            if (salesforceResponse.IsSuccess)
            {
                var record = JsonConvert.DeserializeObject<Dictionary<string, object>>(httpResponse.Response);
                salesforceResponse.Payload = GetId(input.Entity, record);
            }

            return salesforceResponse;
        }

        private List<Dynamics365Entity> ConvertToDynamics365Entitys(Dynamics365EntityResponse dynamics365EntityResponse)
        {
            List<Dynamics365Entity> dynamics365Entitys = new List<Dynamics365Entity>();

            if (dynamics365EntityResponse != null
                && dynamics365EntityResponse.value != null)
            {
                var entitys = dynamics365EntityResponse.value;
                foreach (var entity in entitys)
                {
                    dynamics365Entitys.Add(ConvertToDynamics365Entity(entity));
                }
            }

            return dynamics365Entitys;
        }

        private Dynamics365Entity ConvertToDynamics365Entity(Value value)
        {
            return new Dynamics365Entity()
            {
                Label = Utils.CapitalLettersWithSpaces(value.Label),
                Name = value.LogicalName,
                EntitySetName = value.EntitySetName,
            };
        }

        private async Task<List<Dynamics365Field>> ConvertToDynamics365FieldsAsync(AzureAD azure, Dynamics365EntityDescribeResponse dynamics365EntityDescribeResponse)
        {
            List<Dynamics365Field> dynamics365Fields = new List<Dynamics365Field>();

            if (dynamics365EntityDescribeResponse != null
                && dynamics365EntityDescribeResponse.Attributes != null)
            {
                foreach (var attribute in dynamics365EntityDescribeResponse.Attributes)
                {
                    dynamics365Fields.Add(ConvertToDynamics365Field(attribute));
                }

                await EnrichChoicesAsync(azure, dynamics365EntityDescribeResponse.LogicalName, dynamics365Fields);
            }

            return dynamics365Fields;
        }

        private Dynamics365Field ConvertToDynamics365Field(Model.Attribute attribute)
        {
            return new Dynamics365Field()
            {
                Label = attribute.Label,
                Name = attribute.LogicalName,
                Length = attribute.MaxLength,
                Required = attribute.Required,
                Type = attribute.AttributeType,
                References = attribute.Targets,
                // Choices = ConvertToChoices(attribute.picklistValues),
            };
        }

        private async Task EnrichChoicesAsync(AzureAD azure, string entity, List<Dynamics365Field> dynamics365Fields)
        {
            await LoadAttributeMetadataAsync(azure, entity, dynamics365Fields, "Status");
            await LoadAttributeMetadataAsync(azure, entity, dynamics365Fields, "Picklist");
            await LoadAttributeMetadataAsync(azure, entity, dynamics365Fields, "State");
            await LoadAttributeMetadataAsync(azure, entity, dynamics365Fields, "Boolean");
        }

        private async Task LoadAttributeMetadataAsync(AzureAD azure, string entity, List<Dynamics365Field> dynamics365Fields, string attributeType)
        {
            var attributeMetadataType = $"Microsoft.Dynamics.CRM.{attributeType}AttributeMetadata";
            var attributeMetadatas = dynamics365Fields.Where(f => f.Type == attributeType);
            if (attributeMetadatas != null && attributeMetadatas.Count() > 0)
            {
                var httpResponse = await _webRequestService.SendAsync(azure, GetLoadAttributeMetadataUrl(entity, attributeMetadataType), Enum.EnumHttpMethod.Get);
                if (httpResponse.IsSuccessStatusCode)
                {
                    var choiceResponse = JsonConvert.DeserializeObject<ChoiceResponse>(httpResponse.Response);
                    foreach (var statusAttributeMetadata in attributeMetadatas)
                    {
                        var choices = choiceResponse.value;
                        var choice = choices.FirstOrDefault(f => f.LogicalName == statusAttributeMetadata.Name);
                        if (choice != null)
                        {
                            statusAttributeMetadata.Choices = ConvertToChoices(GetOptions(choice, attributeMetadataType));
                        }
                    }
                }
            }
        }

        private string GetLoadAttributeMetadataUrl(string entity, string attributeMetadataType)
        {
            if ("Microsoft.Dynamics.CRM.BooleanAttributeMetadata" == attributeMetadataType)
            {
                return $"/api/data/v9.0/EntityDefinitions(LogicalName='{entity}')/Attributes/{attributeMetadataType}?$select=LogicalName&$expand=GlobalOptionSet($select=FalseOption,TrueOption)";
            }

            return $"/api/data/v9.0/EntityDefinitions(LogicalName='{entity}')/Attributes/{attributeMetadataType}?$select=LogicalName&$expand=GlobalOptionSet($select=Options)";
        }

        private Option[] GetOptions(Value choice, string attributeMetadataType)
        {
            Option[] Options = choice.GlobalOptionSet.Options;
            if ("Microsoft.Dynamics.CRM.BooleanAttributeMetadata" == attributeMetadataType)
            {
                Options = new Option[] {
                    new Option()
                    {
                        Label = choice.GlobalOptionSet.TrueOption.Label,
                        Value =  choice.GlobalOptionSet.TrueOption.Value,
                    },
                    new Option()
                    {
                        Label = choice.GlobalOptionSet.FalseOption.Label,
                        Value =  choice.GlobalOptionSet.FalseOption.Value,
                    },
                };
            }

            return Options;
        }

        private Choice[] ConvertToChoices(Option[] options)
        {
            Choice[] choices = null;
            if (options != null)
            {
                choices = options.Select(a => new Choice()
                {
                    Label = a.Title,
                    Value = Convert.ToString(a.Value),
                }).ToArray();
            }

            return choices;
        }

        private Dynamics365Response ConvertToDynamics365Response(HttpResponse httpResponse)
        {
            Dynamics365Response dynamics365Response = new Dynamics365Response()
            {
                IsSuccess = httpResponse.IsSuccessStatusCode,
            };

            if (httpResponse != null
                && !httpResponse.IsSuccessStatusCode
                && !string.IsNullOrEmpty(httpResponse.Response))
            {
                dynamics365Response.IsSuccess = false;
                var error = JsonConvert.DeserializeObject<Dynamics365ErrorInfo>(httpResponse.Response);
                dynamics365Response.ErrorMessage = error?.error?.message;
                dynamics365Response.ErrorCode = error?.error?.code;
                _logger.LogWarning(JsonConvert.SerializeObject(httpResponse));
            }

            return dynamics365Response;
        }

        private async Task<HttpResponse> CreateAsync(AzureAD azure, string entitySetName, string content, bool suppressDuplicateDetection = false)
        {
            var headers = new Dictionary<string, string>()
            {
                { "MSCRM.SuppressDuplicateDetection", suppressDuplicateDetection.ToString() },
                { "prefer", "return=representation" },
            };

            var httpResponse = await _webRequestService.SendAsync(azure, $"/api/data/v9.0/{entitySetName}", Enum.EnumHttpMethod.Post, content, headers);
            return httpResponse;
        }

        private async Task<HttpResponse> ExecFetchXmlAsync(AzureAD azure, string entitySetName, string xml)
        {
            var httpResponse = await _webRequestService.SendAsync(azure, $"/api/data/v9.1/{entitySetName}?fetchXml={xml}", Enum.EnumHttpMethod.Get);
            return httpResponse;
        }

        private List<Dynamics365FieldValue> ConvertToDynamics365FieldValues(Dictionary<string, object> record)
        {
            return record?.Select(a => new Dynamics365FieldValue()
            {
                Name = a.Key,
                Value = Convert.ToString(a.Value),
            }).ToList();
        }

        private string BuildFetchXml(SearchInput input, int count = 0)
        {
            CheckSearchInput(input);
            var top = $"count='{count}'";
            if (count == 0)
            {
                top = string.Empty;
            }

            if (!string.IsNullOrEmpty(input.FieldToSearchBy))
            {
                return $"<fetch mapping='logical' output-format='xml-platform' version='1.0' distinct='false' {top}><entity name='{input.Entity}'><filter type='and'><condition value='{input.SearchValue}' attribute='{input.FieldToSearchBy}' operator='eq' /></filter></entity></fetch>";
            }
            else
            {
                return $"<fetch mapping='logical' output-format='xml-platform' version='1.0' distinct='false' {top}><entity name='{input.Entity}'></entity></fetch>";
            }
        }

        private void CheckSearchInput(SearchInput input)
        {
            if (input == null)
            {
                throw new Exception("SearchInput required");
            }
            if (string.IsNullOrEmpty(input.Entity))
            {
                throw new Exception("SearchInput.Entity required");
            }
            if (!string.IsNullOrEmpty(input.FieldToSearchBy)
                && string.IsNullOrEmpty(input.SearchValue))
            {
                throw new Exception("when SearchInput.FieldToSearchBy have a value then SearchInput.SearchValue required");
            }
        }

        private string GetId(string entity, Dictionary<string, object> record)
        {
            var key = $"{entity}id";
            var value = string.Empty;
            if (record != null
                && record.ContainsKey(key))
            {
                value = Convert.ToString(record[key]);
            }

            return value;
        }
    }
}
